package Rangel.firebrigade.extaction;


import adf.agent.action.Action;
import adf.agent.action.common.ActionMove;
import adf.agent.action.fire.ActionExtinguish;
import adf.agent.action.fire.ActionRefill;
import adf.agent.communication.MessageManager;
import adf.agent.develop.DevelopData;
import adf.agent.info.AgentInfo;
import adf.agent.info.ScenarioInfo;
import adf.agent.info.WorldInfo;
import adf.agent.module.ModuleManager;
import adf.agent.precompute.PrecomputeData;
import adf.component.extaction.ExtAction;
import adf.component.module.algorithm.PathPlanning;
import Rangel.common.RangelConstants;
import Rangel.firebrigade.util.FireBrigadeUtilities;
import Rangel.firebrigade.world.RangelFireBrigadeWorld;
import Rangel.common.world.tools.Util;
import Rangel.common.world.helper.RangelWorldHelper;
import Rangel.common.world.entity.RangelBuilding;
import Rangel.common.world.entity.RangelRoad;
import rescuecore2.standard.entities.*;
import rescuecore2.worldmodel.EntityID;

import java.util.*;

import static rescuecore2.misc.Handy.objectsToIDs;
import static rescuecore2.standard.entities.StandardEntityURN.HYDRANT;
import static rescuecore2.standard.entities.StandardEntityURN.REFUGE;

public class ActionFireFighting extends ExtAction {
    private PathPlanning pathPlanning;

    private int maxExtinguishDistance;
    private ExtAction actionExtMove;
    private static final int EXTINGUISH_DISTANCE_THRESHOLD = 5000;
    private RangelFireBrigadeWorld worldHelper;
    private EntityID target;
    private Action lastAction = null;
    public ActionFireFighting(AgentInfo agentInfo, WorldInfo worldInfo, ScenarioInfo scenarioInfo, ModuleManager moduleManager, DevelopData developData) {
        super(agentInfo, worldInfo, scenarioInfo, moduleManager, developData);
        this.maxExtinguishDistance = scenarioInfo.getFireExtinguishMaxDistance();

        this.target = null;
        switch (scenarioInfo.getMode()) {
            case PRECOMPUTATION_PHASE:
            case PRECOMPUTED:
            case NON_PRECOMPUTE:
                this.pathPlanning = moduleManager.getModule("ActionFireFighting.PathPlanning", "adf.sample.module.algorithm.SamplePathPlanning");
                this.actionExtMove = moduleManager.getExtAction("TacticsFireBrigade.ActionExtMove", "adf.sample.extaction.ActionExtMove");
                break;
        }

        this.worldHelper = (RangelFireBrigadeWorld) RangelWorldHelper.load(agentInfo, worldInfo, scenarioInfo, moduleManager, developData);

    }

    @Override
    public ExtAction precompute(PrecomputeData precomputeData) {
        super.precompute(precomputeData);
        if (this.getCountPrecompute() >= 2) {
            return this;
        }
        this.pathPlanning.precompute(precomputeData);
        this.actionExtMove.precompute(precomputeData);

        return this;
    }

    @Override
    public ExtAction resume(PrecomputeData precomputeData) {
        super.resume(precomputeData);
        if (this.getCountResume() >= 2) {
            return this;
        }

        fillProperties();
        this.worldHelper.resume(precomputeData);
        this.pathPlanning.resume(precomputeData);
        this.actionExtMove.resume(precomputeData);

        return this;
    }

    @Override
    public ExtAction preparate() {
        super.preparate();
        if (this.getCountPreparate() >= 2) {
            return this;
        }

        this.worldHelper.preparate();
        this.pathPlanning.preparate();
        this.actionExtMove.preparate();
        fillProperties();
        return this;
    }

    @Override
    public ExtAction updateInfo(MessageManager messageManager) {
        super.updateInfo(messageManager);
        if (this.getCountUpdateInfo() >= 2) {
            return this;
        }
        this.worldHelper.updateInfo(messageManager);
        this.pathPlanning.updateInfo(messageManager);
        this.actionExtMove.updateInfo(messageManager);
        return this;
    }

    @Override
    public ExtAction setTarget(EntityID target) {
        this.target = null;
        if (target != null) {
            StandardEntity entity = this.worldInfo.getEntity(target);
            if (entity instanceof Building) {
                this.target = target;
            }
        }
        return this;
    }

    @Override
    public ExtAction calc() {
        this.result = null;
        FireBrigade agent = (FireBrigade) this.agentInfo.me();

        Action action = moveToRefugeIfDamagedOrTankIsEmpty();//去避难所，当受伤或者没水
        if (action != null) {
            this.result = action;
            return this;
        }

        if (this.target == null) {
            return this;
        }
        this.result = this.calcExtinguish(agent, this.pathPlanning, this.target);
        return this;
    }

    private void fillProperties() {
        for (RangelBuilding RangelBuilding : worldHelper.getRangelBuildings()) {
            Set<EntityID> extinguishableFromAreas = FireBrigadeUtilities.findAreaIDsInExtinguishRange(worldInfo, scenarioInfo, RangelBuilding.getID());
            List<RangelBuilding> buildingsInExtinguishRange = new ArrayList<>();
            for (EntityID next : extinguishableFromAreas) {
                if (worldInfo.getEntity(next) instanceof Building) {
                    buildingsInExtinguishRange.add(worldHelper.getRangelBuilding(next));
                }
            }
            RangelBuilding.setExtinguishableFromAreas(extinguishableFromAreas);
        }

    }


    private Action calcExtinguish(FireBrigade agent, PathPlanning pathPlanning, EntityID target) {
        EntityID agentPosition = agent.getPosition();
        StandardEntity positionEntity = Objects.requireNonNull(this.worldInfo.getPosition(agent));

        List<StandardEntity> neighbourBuilding = new ArrayList<>();
        StandardEntity entity = this.worldInfo.getEntity(target);
        if (entity instanceof Building) {
            if (this.worldInfo.getDistance(positionEntity, entity) < this.maxExtinguishDistance) {
                neighbourBuilding.add(entity);
            }
        }

        if (neighbourBuilding.size() > 0) {
            neighbourBuilding.sort(new DistanceSorter(this.worldInfo, agent));

            int waterPower;
            if (worldHelper != null) {
                RangelBuilding targetRangelBuilding = worldHelper.getRangelBuilding(target);
                waterPower = FireBrigadeUtilities.calculateWaterPower(agentInfo.getWater(), scenarioInfo.getFireExtinguishMaxSum(), targetRangelBuilding);
                targetRangelBuilding.increaseWaterQuantity(waterPower);
            } else {
                waterPower = FireBrigadeUtilities.calculateWaterPowerNotEstimated(agentInfo.getWater(), scenarioInfo.getFireExtinguishMaxSum(), (Building) worldInfo.getEntity(target));
            }

            return new ActionExtinguish(neighbourBuilding.get(0).getID(), waterPower);
        }

        StandardEntity bestLocation = chooseBestLocationToStandForExtinguishingFire(target);
        if (bestLocation == null) {
            return null;
        }
        List<EntityID> movePlan = pathPlanning.setFrom(agentPosition).setDestination(bestLocation.getID()).calc().getResult();
        if (movePlan == null || movePlan.isEmpty()) {
            Collection<StandardEntity> inRange = worldInfo.getObjectsInRange(bestLocation, maxExtinguishDistance / 3);
            int counter = 0;
            for (StandardEntity e : inRange) {
                if (e instanceof Area && worldInfo.getDistance(target, e.getID()) < maxExtinguishDistance) {

                    movePlan = pathPlanning.setFrom(agentPosition).setDestination(e.getID()).calc().getResult();
                    counter++;
                    if (movePlan != null && !movePlan.isEmpty()) {
                        return getMoveAction(movePlan);
                    }
                    if (counter > 3) {
                        lastTryToExtinguish();
                    }
                }
            }
        }
        return this.getMoveAction(pathPlanning, agentPosition, bestLocation.getID());
    }


    private void lastTryToExtinguish() {
        Set<StandardEntity> buildingsInMyExtinguishRange = getBuildingsInMyExtinguishRange();
        List<StandardEntity> fieryBuildingsInMyExtinguishRange = new ArrayList<>();
        for (StandardEntity entity : buildingsInMyExtinguishRange) {
            Building building = (Building) entity;
            if (building.isOnFire()) {
                fieryBuildingsInMyExtinguishRange.add(entity);
            }
        }
        StandardEntity tempTarget = findNearest(fieryBuildingsInMyExtinguishRange, agentInfo.getPositionArea());
        if (tempTarget != null) {
            int waterPower;
            if (worldHelper != null) {
                RangelBuilding targetRangelBuilding = worldHelper.getRangelBuilding(tempTarget.getID());
                waterPower = FireBrigadeUtilities.calculateWaterPower(agentInfo.getWater(), scenarioInfo.getFireExtinguishMaxSum(), targetRangelBuilding);
                targetRangelBuilding.increaseWaterQuantity(waterPower);
            } else {
                waterPower = FireBrigadeUtilities.calculateWaterPowerNotEstimated(agentInfo.getWater(), scenarioInfo.getFireExtinguishMaxSum(), (Building) worldInfo.getEntity(target));
            }
            new ActionExtinguish(tempTarget.getID(), waterPower);
        }
    }

    public Set<StandardEntity> getBuildingsInMyExtinguishRange() {
        Set<StandardEntity> result = new HashSet<>();
        int maxExtinguishDistance = this.maxExtinguishDistance - EXTINGUISH_DISTANCE_THRESHOLD;
        for (StandardEntity next : worldInfo.getObjectsInRange(agentInfo.getID(), (int) (maxExtinguishDistance * 1.5))) {
            if (next instanceof Building) {
                if (worldInfo.getDistance(next.getID(), agentInfo.getID()) < maxExtinguishDistance) {
                    result.add(next);
                }
            }
        }
        return result;
    }

    public StandardEntity findNearest(List<StandardEntity> buildings, StandardEntity basePosition) {
        StandardEntity result = null;
        double minDistance = Double.POSITIVE_INFINITY;
        for (StandardEntity next : buildings) {
            double dist = worldInfo.getDistance(next.getID(), basePosition.getID());
            if (dist < minDistance) {
                result = next;
                minDistance = dist;
            }
        }
        return result;

    }
//从所有火灾位置选择最近的位置，当作下一次的最佳灭火目标
    private StandardEntity chooseBestLocationToStandForExtinguishingFire(EntityID target) {
        double minDistance = Integer.MAX_VALUE;
        Set<EntityID> forbiddenLocationIDs = getForbiddenLocations();
        List<StandardEntity> possibleAreas = new ArrayList<>();
        StandardEntity targetToExtinguish = null;
        double dis;
        Set<EntityID> extinguishableFromAreas = worldHelper.getRangelBuilding(target).getExtinguishableFromAreas();
        for (EntityID next : extinguishableFromAreas) {
            StandardEntity entity = worldInfo.getEntity(next);
            possibleAreas.add(entity);
        }

        List<StandardEntity> forbiddenLocations = worldHelper.getEntities(forbiddenLocationIDs);
        possibleAreas.removeAll(forbiddenLocations);
        if (possibleAreas.isEmpty()) {
            for (EntityID next : extinguishableFromAreas) {
                possibleAreas.add(worldInfo.getEntity(next));
            }
        }

        for (StandardEntity entity : possibleAreas) {
            if (entity instanceof Road) {
                dis = worldInfo.getDistance(agentInfo.me(), entity);
                if (dis < minDistance) {
                    minDistance = dis;
                    targetToExtinguish = entity;
                }
            }
        }

        if (targetToExtinguish == null) {
            for (StandardEntity entity : possibleAreas) {
                if (entity instanceof Building) {
                    Building building = (Building) entity;
                    dis = worldInfo.getDistance(agentInfo.me(), entity);
                    if (dis < minDistance && (!building.isFierynessDefined() || (building.isFierynessDefined() && (building.getFieryness() >= 4 || building.getFieryness() <= 1)))) {
                        minDistance = dis;
                        targetToExtinguish = entity;
                    }
                }
            }
        }
        return targetToExtinguish;
    }


    private Set<EntityID> getForbiddenLocations() {
        Set<EntityID> forbiddenLocations = new HashSet<>();

        Collection<StandardEntity> clusterEntities = this.worldHelper.getFireClustering().getClusterEntities(this.worldHelper.getFireClustering().getMyClusterIndex());

        if (clusterEntities != null) {
            forbiddenLocations.addAll(objectsToIDs(clusterEntities));
        }
        forbiddenLocations.addAll(worldInfo.getFireBuildingIDs());
        int n = 3;
        if (worldHelper.isMapMedium()) n = 10;
        if (worldHelper.isMapHuge()) n = 15;
        for (StandardEntity next : worldInfo.getEntitiesOfType(StandardEntityURN.FIRE_BRIGADE)) {
            if (agentInfo.getID().getValue() < next.getID().getValue() && worldInfo.getDistance(agentInfo.getID(), next.getID()) < scenarioInfo.getPerceptionLosMaxDistance() && --n <= 0) {
                StandardEntity position = worldInfo.getPosition(next.getID());
                if (position != null) {
                    RangelBuilding buildingOfNearFB = worldHelper.getRangelBuilding(position.getID());
                    RangelRoad roadOfNearFB = worldHelper.getRangelRoad(position.getID());
                    if (roadOfNearFB != null) {
                        forbiddenLocations.addAll(roadOfNearFB.getObservableAreas());
                    }
                    if (buildingOfNearFB != null) {
                        forbiddenLocations.addAll(buildingOfNearFB.getObservableAreas());
                    }
                }
            }
        }
        return forbiddenLocations;
    }


    private Action getMoveAction(PathPlanning pathPlanning, EntityID from, EntityID target) {
        pathPlanning.setFrom(from);
        pathPlanning.setDestination(target);
        List<EntityID> path = pathPlanning.calc().getResult();
        return getMoveAction(path);
    }

    private Action getMoveAction(List<EntityID> path) {
        if (path != null && path.size() > 0) {
            StandardEntity entity = this.worldInfo.getEntity(path.get(path.size() - 1));
            if (entity instanceof Building) {
                if (entity.getStandardURN() != StandardEntityURN.REFUGE) {
                    path.remove(path.size() - 1);
                }
            }
            if (!path.isEmpty()) {
                return actionExtMove.setTarget(path.get(path.size() - 1)).calc().getAction();
            }
            return null;
        }
        return null;
    }

    private static class DistanceSorter implements Comparator<StandardEntity> {
        private StandardEntity reference;
        private WorldInfo worldInfo;

        DistanceSorter(WorldInfo wi, StandardEntity reference) {
            this.reference = reference;
            this.worldInfo = wi;
        }

        public int compare(StandardEntity a, StandardEntity b) {
            int d1 = this.worldInfo.getDistance(this.reference, a);
            int d2 = this.worldInfo.getDistance(this.reference, b);
            return d1 - d2;
        }
    }


    private boolean moveToHydrantFail = false;
    private int failTime = 0;
    protected int timeInThisHydrant = 0;
    protected EntityID lastHydrant = null;
    protected int stayInHydrant = 15;
    protected int currentWater = 0;
    protected int prevWater = 0;

    protected Action moveToRefugeIfDamagedOrTankIsEmpty() {
        int requiredWater;//所需水量
        if (worldHelper.getRefuges().isEmpty() && worldHelper.getHydrants().isEmpty()) {//判断有没有避难所或消防栓
            return null;
        }
        FireBrigade self = (FireBrigade) worldInfo.getEntity(agentInfo.getID());//获取自身信息

        StandardEntityURN positionURN = agentInfo.getPositionArea().getStandardURN();//得到自身所在区域的类型


        if (lastHydrant == null || !worldHelper.getSelfPosition().getID().equals(lastHydrant)) {//是否已在消防栓
            timeInThisHydrant = 0;
            lastHydrant = null;
        }
        prevWater = currentWater;//之前水量
        currentWater = self.getWater();//当前水量

        if (worldHelper.getTime() - failTime > RangelConstants.AVAILABLE_HYDRANTS_UPDATE_TIME) {//消防栓还未更新，处于不可用状态
            moveToHydrantFail = false;
        }
        if (positionURN == REFUGE) {//已在避难所

            requiredWater = worldHelper.isWaterRefillRateInRefugeSet ?
                    worldHelper.getWaterRefillRate() * 10 :     //10 Cycle in refuge
                    worldHelper.getMaxWater() / 3 * 2;          // 2/3 tank
            requiredWater = Util.min(worldHelper.getMaxWater(), requiredWater);
            if (self.isWaterDefined() && self.getWater() < requiredWater) {
                if (!worldHelper.isWaterRefillRateInRefugeSet) {
                    int refillRate = Math.abs(currentWater - prevWater);
                    if (refillRate > 0 && !(lastAction instanceof ActionExtinguish)) {
                        worldHelper.setWaterRefillRate(refillRate);
                        worldHelper.isWaterRefillRateInRefugeSet = true;
                    }
                }
                return new ActionRefill();
            }
        } else if (positionURN == HYDRANT) {
            if (!worldHelper.isWaterRefillRateInHydrantSet) {
                int refillRate = Math.abs(currentWater - prevWater);
                if (refillRate <= 0) {
                } else if (!(lastAction instanceof ActionExtinguish)) {
                    worldHelper.isWaterRefillRateInHydrantSet = true;
                }
            }
            boolean havePermission;
            havePermission = worldHelper.getAvailableHydrants().contains(worldHelper.getSelfPosition());
            if (havePermission) {
                lastHydrant = self.getPosition();
                requiredWater = Math.min(worldHelper.getMaxPower() * 3, worldHelper.getMaxWater() * 2 / 3);
                if (self.isWaterDefined() && self.getWater() < requiredWater) {
                    if (timeInThisHydrant < stayInHydrant) {
                        timeInThisHydrant++;
                        List<EntityID> path = new ArrayList<>();
                        path.add(self.getPosition());
                        new ActionMove(path);
                    } else {
                        failTime = worldHelper.getTime();
                        moveToHydrantFail = true;
                    }
                }
            }
        }

        if (worldHelper.getRefuges().size() > 0 && (self.getDamage() > 10 && self.getHP() < 5000) || self.getWater() == 0) {
            Action action = moveToRefuge();//todo
            if (action != null) {
                return action;
            }
        }

        if (worldHelper.getHydrants().size() > 0 && self.getWater() <= 0 && !moveToHydrantFail) {
            return moveToHydrant();
        }

        return null;

    }

    private Action moveToRefuge() {
        Area selfPositionArea = (Area) worldInfo.getEntity(agentInfo.getPosition());
        List<EntityID> refugePath = getRefugePath(selfPositionArea);
        if (refugePath == null || refugePath.isEmpty()) {
            return null;
        }
        return new ActionMove(refugePath);
    }

    private Action moveToHydrant() {
        Area selfPositionArea = (Area) worldInfo.getEntity(agentInfo.getPosition());
        List<EntityID> hydrantPath = getHydrantPath(selfPositionArea);
        if (hydrantPath == null || hydrantPath.isEmpty()) {
            return null;
        }
        return new ActionMove(hydrantPath);
    }

    private List<EntityID> getRefugePath(Area sourceArea) {
        return getAreaTypePath(sourceArea, Math.min(10, worldHelper.getRefuges().size()), Refuge.class, worldHelper.getRefuges());
    }

    private List<EntityID> getHydrantPath(Area sourceArea) {
        return getAreaTypePath(sourceArea, Math.min(10, worldHelper.getHydrants().size()), Hydrant.class, worldHelper.getAvailableHydrants());
    }

    @Override
    public Action getAction() {
        return lastAction = result;
    }


    private List<EntityID> getAreaTypePath(Area sourceArea, int numberToGetNearest, Class<? extends StandardEntity> areaType, Collection<? extends StandardEntity> validAreas) {

        List<EntityID> finalPath = new ArrayList<>();

        if (validAreas.isEmpty() || areaType.isInstance(sourceArea)) {
            return finalPath;
        }

        List<EntityID> bestAreaPath = new ArrayList<>();
        Area bestArea = null;
        double minimumCost = Double.MAX_VALUE;
        List<EntityID> path;
        double cost;

        List<Area> nearestAreas;
        nearestAreas = getSomeNearAreaType(sourceArea.getX(), sourceArea.getY(), numberToGetNearest, validAreas);

        for (Area area : nearestAreas) {
            if (area == null) {
                continue;
            }

            if (sourceArea.equals(area)) {
                return null;
            }

            pathPlanning.setFrom(agentInfo.getPosition()).setDestination(area.getID()).calc();
            path = pathPlanning.getResult();
            if (path != null && !path.isEmpty()) {
                cost = pathPlanning.getDistance();

                if (cost < minimumCost) {
                    minimumCost = cost;
                    bestArea = area;
                    bestAreaPath = new ArrayList<>(path);
                }
            } else {
                worldHelper.flagUnreachable(area);
            }
        }

        if (bestArea == null && !nearestAreas.isEmpty()) {
            bestArea = nearestAreas.get(0);
            bestAreaPath = pathPlanning.setFrom(agentInfo.getPosition()).setDestination(bestArea.getID()).calc().getResult();

        }


        finalPath = bestAreaPath;
        return finalPath;
    }


    private List<Area> getSomeNearAreaType(int selfX, int selfY, int numberToGetNearest, Collection<? extends StandardEntity> areas) {
        List<Area> nearAreas = new ArrayList<>();
        List<Area> allRefuges = new ArrayList<>();
        for (StandardEntity standardEntity : areas) {
            Area refuge = (Area) standardEntity;
            allRefuges.add(refuge);
        }
        Area selected;
        Area absoluteNearestArea = null;
        int absoluteMinDist = Integer.MAX_VALUE;
        int minDistance, distance, maxDistance = -1;

        StandardEntity self = worldInfo.getEntity(agentInfo.getID());

        while (nearAreas.size() < numberToGetNearest) {

            minDistance = Integer.MAX_VALUE;
            selected = null;

            for (Area area : allRefuges) {
                distance = Util.distance(selfX, selfY, area.getX(), area.getY());
                if (distance < minDistance && (area instanceof Building && worldHelper.getRangelBuilding(area.getID()).isOneEntranceOpen(worldHelper))) {
                    minDistance = distance;
                    selected = area;
                    absoluteNearestArea = area;
                }
                if (absoluteMinDist > distance) {
                    absoluteMinDist = distance;
                    absoluteNearestArea = area;
                }
            }
            if (self instanceof PoliceForce && absoluteNearestArea != null) {
                break;
            }
            if (maxDistance == -1) {
                maxDistance = minDistance * 2;
            } else if (maxDistance < minDistance) {
                break;
            }
            if (selected != null) {
                allRefuges.remove(selected);
                nearAreas.add(selected);
            }
        }
        if (nearAreas.isEmpty()) {
            nearAreas.add(absoluteNearestArea);
        }
        return nearAreas;
    }
}

